/*
 * Copyright (c) 2019 Author: Fancyears Milos Malvis @1243093366@qq.com
 * ALL COPYRIGHT REVERSED.
 */

package com.fancyears.service.impl.encryption;

import com.fancyears.dao.i.user.UserDao;
import com.fancyears.global.GlobalSetting;
import com.fancyears.model.user.User;
import com.fancyears.service.i.encryption.RSAService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.Cipher;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.StringWriter;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.fancyears.global.GlobalSetting.*;

/**
 * 用户加密登录
 * <p>
 * 生成公私钥，私钥存储在redis中，公钥存放到session的结果中，登录成功后私钥会进行删除操作
 *
 * @Author: Fancyears Milos Malvis
 * @Date: Created in  2019/6/20 17:32
 * @Modified By:
 */
@Service
@Slf4j
public class RSAServiceImpl implements RSAService {

    /**
     * 生成长度，不一定是512，可以上1024或2048
     */
    private int keySize = 1024;
    /**
     * 加密实现的算法
     */
    private String algorithm = "RSA";
    /**
     * 公钥代号
     */
    private String publicKeyDefine = "publicKey";
    /**
     * 私钥代号
     */
    private String privateKeyDefine = "privateKey";
    /**
     * redis缓存
     */
    @Autowired
    RedisTemplate redisTemplate;
    /**
     * 用户查询dao层
     */
    @Autowired
    UserDao userDao;


    static {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }

    /**
     * 获取session公钥
     * <p>
     * 首先生成钥匙对
     * <p>
     * 然后根据钥匙对生成公私钥，私钥存储到redis中，公钥作为返回结果返回给前端
     *
     * @return
     */
    @Override
    public Map<String, String> getSession(HttpSession clientSession) throws NoSuchAlgorithmException, IOException {
        Map<String, String> result = new HashMap<>();
        //生成存储的键
        String redisKey = ENCRYPT_FOLDER_CODE + clientSession.getId();
        log.error("查询的key="+redisKey);
        //TODO 这一段查询可能效率比较低，是否考虑放到session中，如果redis不是集群的速度会很快
        if (redisTemplate.hasKey(redisKey) && redisTemplate.opsForHash().hasKey(redisKey, publicKeyDefine)) {
            result.put(publicKeyDefine, (String) redisTemplate.opsForHash().get(redisKey, publicKeyDefine));
            return result;
        }
        //公钥
        String publicKey = null;
        //私钥
        String privateKey = null;
        //生成钥匙对
        KeyPair kp = getKeyPair();
        //生成私钥
        privateKey = getPrivateKey(kp);
        //生成公钥
        publicKey = getPublicKey(kp);
        // 记录私钥，30分钟不进行登录就需要刷新页面
        Map<String, String> keys = new HashMap<>();
        keys.put(publicKeyDefine, publicKey);
        keys.put(privateKeyDefine, privateKey);
        //存储公私钥对
        redisTemplate.opsForHash().putAll(redisKey, keys);
        //设置失效时间，可能不太可靠
        redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
        // 返回公钥
        result.put(publicKeyDefine, publicKey);
        return result;
    }

    @Override
    public Map<String, Object> loginByEncrypt(Map<String, String> params, HttpSession clientSession) {
        Map<String, Object> result = new HashMap<>();
        /** redis中的钥匙对键*/
        String redisKey = ENCRYPT_FOLDER_CODE + clientSession.getId();

        String privateKey = (String) redisTemplate.opsForHash().get(redisKey, privateKeyDefine);

        /**私钥失效，需要刷新页面，触发{@link RSAService#getSession(HttpSession)}重新生成公钥，*/
        if (StringUtils.isEmpty(privateKey)) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.FAILED);
            result.put(MESSAGE, "登录失败，请重试");//非法参数，前端不能知道这是非法参数
            return result;
        }


        if (!params.containsKey("payload")) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.ILLEGAL_PARAMETER);
            result.put(MESSAGE, "用户名或密码错误");
            return result;
        }


        Map<String, String> json = null;
        try {
            String payload = params.get("payload");
            String text = decrypt(payload, privateKey);

            ObjectMapper mapper = new ObjectMapper();
            json = mapper.readValue(text, Map.class);//转换成对象
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (json == null) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.ILLEGAL_PARAMETER);
            result.put(MESSAGE, "登录失败，请重试");//非法参数，前端不能知道这是非法参数
            return result;
        }
        String username = json.get("account");
        String password = json.get("password");

        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            result.put(STATUS, STATUS_ENUM.USERNAME_OR_PASSWORD_ERROR);
            result.put(MESSAGE, "用户名或密码错误");
            return result;
        }

        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//        password = bCryptPasswordEncoder.encode(password);
        User user = userDao.selectByAccountName(username);
        if (user == null) {
            result.put(STATUS, STATUS_ENUM.USERNAME_OR_PASSWORD_ERROR);
            result.put(MESSAGE, "用户名或密码错误");
            return result;
        }
        //匹配密码是否正确
        if (!bCryptPasswordEncoder.matches(password, user.getPassword())) {
            result.put(STATUS, STATUS_ENUM.USERNAME_OR_PASSWORD_ERROR);
            result.put(MESSAGE, "用户名或密码错误");
            return result;
        }
        //将这个私钥删除，下次重新登录需要重新生成公私钥
        redisTemplate.delete(redisKey);
        //存储用户名
        clientSession.setAttribute(GlobalSetting.USERNAME, user.getUsername());

        result.put(STATUS, STATUS_ENUM.OK);
        result.put(MESSAGE, "登录成功");
        result.put(USERNAME, user.getUsername());

        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Map<String, Object> updatePassword(Map<String, String> params, HttpSession clientSession) {
        Map<String, Object> result = new HashMap<>();
        /** redis中的钥匙对键*/
        String redisKey = ENCRYPT_FOLDER_CODE + clientSession.getId();

        String privateKey = (String) redisTemplate.opsForHash().get(redisKey, privateKeyDefine);

        /**私钥失效，需要刷新页面，触发{@link RSAService#getSession(HttpSession)}重新生成公钥，*/
        if (StringUtils.isEmpty(privateKey)) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.FAILED);
            result.put(MESSAGE, "更新失败，请重试");//非法参数，前端不能知道这是非法参数
            return result;
        }


        if (!params.containsKey("payload")) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.ILLEGAL_PARAMETER);
            result.put(MESSAGE, "更新失败，请重试");
            return result;
        }


        Map<String, String> json = null;
        try {
            String payload = params.get("payload");
            String text = decrypt(payload, privateKey);

            ObjectMapper mapper = new ObjectMapper();
            json = mapper.readValue(text, Map.class);//转换成对象
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (json == null) {
            result.put(STATUS, GlobalSetting.STATUS_ENUM.ILLEGAL_PARAMETER);
            result.put(MESSAGE, "更新失败，请重试");//非法参数，前端不能知道这是非法参数
            return result;
        }
        String username = json.get("account");
        String password = json.get("password");

        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            result.put(STATUS, STATUS_ENUM.USERNAME_OR_PASSWORD_ERROR);
            result.put(MESSAGE, "更新失败，请重试");
            return result;
        }

        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        password = bCryptPasswordEncoder.encode(password);
        User user = new User(username, password);
        if (userDao.updatePassword(user) > 0) {
            result.put(STATUS, STATUS_ENUM.OK);
            result.put(MESSAGE, "更新密码成功");
            result.put(USERNAME, user.getUsername());
            throw new RuntimeException("RSAServer 更新失败");
        } else {
            //throw new RuntimeException("RSAServer 更新密码失败");
        }
        return result;
    }

    /*******************************可以做成Util******************************/
    @Override
    public String decrypt(String encode, String privateKey) throws Exception {
        String keyText = privateKey.split("-----")[2].replaceAll("\n", "").replaceAll("\r", "");
        byte[] bytes = Base64.decode(keyText.getBytes());
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(bytes);
        PrivateKey key = keyFactory.generatePrivate(privateKeySpec);

        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] doFinal = cipher.doFinal(Base64.decode(encode));
        return new String(doFinal, "utf-8");
    }

    @Override
    public KeyPair getKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(algorithm);
        keyPairGen.initialize(keySize);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        return keyPair;
    }

    @Override
    public String getPrivateKey(KeyPair keyPair) throws IOException {
        String privateKey;
        byte[] encoded = keyPair.getPrivate().getEncoded();
        PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(encoded);
        ASN1Encodable encodable = pkInfo.parsePrivateKey();
        ASN1Primitive primitive = encodable.toASN1Primitive();

        byte[] privateKeyPKCS1 = primitive.getEncoded();
        PemObject pemObject = new PemObject("RSA PRIVATE KEY", privateKeyPKCS1);
        privateKey = printKey(pemObject);
        return privateKey;
    }

    @Override
    public String getPublicKey(KeyPair keyPair) throws IOException {
        String publicKey;
        byte[] encoded = keyPair.getPublic().getEncoded();
        SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo.getInstance(encoded);
        ASN1Primitive primitive = spkInfo.parsePublicKey();
        byte[] publicKeyPKCS1 = primitive.getEncoded();

        PemObject pemObject = new PemObject("RSA PUBLIC KEY", publicKeyPKCS1);
        publicKey = printKey(pemObject);
        return publicKey;
    }

    /**
     * 打印出key
     *
     * @param pemObject
     * @return
     * @throws IOException
     */
    private String printKey(PemObject pemObject) throws IOException {
        try (StringWriter stringWriter = new StringWriter()) {
            try (PemWriter pemWriter = new PemWriter(stringWriter)) {
                pemWriter.writeObject(pemObject);
                pemWriter.flush();
                String pemString = stringWriter.toString();
                return pemString;
            }
        }
    }
}
